React์์ ๊ฐ๋ ฅํ ์ต์ ํผ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ๊ตฌํํ์ธ์. ์ด ๊ฐ์ด๋๋ experimental_useFormStatus ํ ๊ณผ ์๋ฒ ์ก์ ์ ํตํด ๊ฒฌ๊ณ ํ๊ณ ์ฑ๋ฅ์ด ๋ฐ์ด๋ ํผ์ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ์ฌ์ธต์ ์ผ๋ก ๋ค๋ฃน๋๋ค.
React์ `experimental_useFormStatus`๋ก ํผ ์ ํจ์ฑ ๊ฒ์ฌ ๋ง์คํฐํ๊ธฐ
ํผ์ ์น ์ํธ์์ฉ์ ๊ธฐ๋ฐ์ ๋๋ค. ๊ฐ๋จํ ๋ด์ค๋ ํฐ ๊ตฌ๋ ๋ถํฐ ๋ณต์กํ ๋ค๋จ๊ณ ๊ธ์ต ์ ํ๋ฆฌ์ผ์ด์ ์ ์ด๋ฅด๊ธฐ๊น์ง, ํผ์ ์ฌ์ฉ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ๊ณผ ์ํตํ๋ ์ฃผ์ ์ฑ๋์ ๋๋ค. ๊ทธ๋ฌ๋ ์๋ ๊ฐ React์์ ํผ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๊ฒ์ ๋ณต์ก์ฑ, ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋, ๊ทธ๋ฆฌ๊ณ ์์กด์ฑ์ ๋ํ ํผ๋ก์ ์์ฒ์ด์์ต๋๋ค. ์ฐ๋ฆฌ๋ ์ ์ด ์ปดํฌ๋ํธ(controlled components)๋ฅผ ๋ค๋ฃจ๊ณ , ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์จ๋ฆํ๋ฉฐ, ์ํํ๊ณ ์ง๊ด์ ์ธ ์ฌ์ฉ์ ๊ฒฝํ์ ์ถ๊ตฌํ๊ธฐ ์ํด ์๋ง์ `onChange` ํธ๋ค๋ฌ๋ฅผ ์์ฑํด์์ต๋๋ค.
React ํ์ ์น ๊ฐ๋ฐ์ ์ด๋ฌํ ๊ทผ๋ณธ์ ์ธ ์ธก๋ฉด์ ์ฌ๊ณ ํด์๊ณ , ์ด๋ React ์๋ฒ ์ก์ (React Server Actions)์ ์ค์ฌ์ผ๋ก ํ ์๋กญ๊ณ ๊ฐ๋ ฅํ ํจ๋ฌ๋ค์์ ๋์ ์ผ๋ก ์ด์ด์ก์ต๋๋ค. ์ ์ง์ ํฅ์(progressive enhancement) ์์น์ ๊ธฐ๋ฐํ ์ด ์๋ก์ด ๋ชจ๋ธ์ ๋ก์ง์ ๊ทธ๊ฒ์ด ์ํ ๊ณณ, ์ฆ ์ข ์ข ์๋ฒ์ ๋ ๊ฐ๊น๊ฒ ์ด๋์ํด์ผ๋ก์จ ํผ ์ฒ๋ฆฌ๋ฅผ ๋จ์ํํ๋ ๊ฒ์ ๋ชฉํ๋ก ํฉ๋๋ค. ์ด ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ํ๋ช ์ ์ค์ฌ์๋ ๋ ๊ฐ์ง ์๋ก์ด ์คํ์ ํ ์ด ์์ต๋๋ค: `useFormState`์ ์ค๋ ๋ ผ์์ ์ฃผ์ธ๊ณต์ธ `experimental_useFormStatus`์ ๋๋ค.
์ด ์ข ํฉ ๊ฐ์ด๋์์๋ `experimental_useFormStatus` ํ ์ ๋ํด ์ฌ์ธต์ ์ผ๋ก ์์๋ณผ ๊ฒ์ ๋๋ค. ๋จ์ํ ๋ฌธ๋ฒ๋ง ์ดํด๋ณด๋ ๊ฒ์ด ์๋๋ผ, ์ด ํ ์ด ๊ฐ๋ฅํ๊ฒ ํ๋ ์ฌ๊ณ ๋ชจ๋ธ์ธ ์ํ ๊ธฐ๋ฐ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง(Status-Based Validation Logic)์ ํ๊ตฌํ ๊ฒ์ ๋๋ค. ์ด ํ ์ด ์ด๋ป๊ฒ UI๋ฅผ ํผ ์ํ๋ก๋ถํฐ ๋ถ๋ฆฌํ๊ณ , ๋ณด๋ฅ(pending) ์ํ ๊ด๋ฆฌ๋ฅผ ๋จ์ํํ๋ฉฐ, ์๋ฒ ์ก์ ๊ณผ ํ๋ ฅํ์ฌ ์๋ฐ์คํฌ๋ฆฝํธ๊ฐ ๋ก๋๋๊ธฐ ์ ์๋ ์๋ํ๋ ๊ฒฌ๊ณ ํ๊ณ ์ ๊ทผ์ฑ์ด ๋์ผ๋ฉฐ ์ฑ๋ฅ์ด ๋ฐ์ด๋ ํผ์ ๋ง๋๋์ง ๋ฐฐ์ฐ๊ฒ ๋ ๊ฒ์ ๋๋ค. React์์ ํผ์ ๋ง๋๋ ๊ฒ์ ๋ํด ์๊ณ ์๋ ๋ชจ๋ ๊ฒ์ ๋ค์ ์๊ฐํ ์ค๋น๋ฅผ ํ์ญ์์ค.
ํจ๋ฌ๋ค์์ ์ ํ: React ํผ์ ์งํ
`useFormStatus`๊ฐ ๊ฐ์ ธ์จ ํ์ ์ ์์ ํ ์ดํดํ๋ ค๋ฉด, ๋จผ์ React ์ํ๊ณ์์ ํผ ๊ด๋ฆฌ์ ์ฌ์ ์ ์ดํดํด์ผ ํฉ๋๋ค. ์ด ๋งฅ๋ฝ์ ์ด ์๋ก์ด ์ ๊ทผ ๋ฐฉ์์ด ์ผ๋ง๋ ์ฐ์ํ๊ฒ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ๋์ง ๋ณด์ฌ์ค๋๋ค.
๊ณผ๊ฑฐ์ ๋ฐฉ์: ์ ์ด ์ปดํฌ๋ํธ์ ์๋ํํฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
์๋ ๊ฐ React ํผ์ ํ์ค ์ ๊ทผ ๋ฐฉ์์ ์ ์ด ์ปดํฌ๋ํธ(controlled component) ํจํด์ด์์ต๋๋ค. ์ด๋ ๋ค์์ ํฌํจํฉ๋๋ค:
- ๊ฐ ํผ ์ ๋ ฅ์ ๊ฐ์ ๋ด๊ธฐ ์ํด React ์ํ ๋ณ์(์: `useState`์์ ์จ)๋ฅผ ์ฌ์ฉํฉ๋๋ค.
- ๋ชจ๋ ํค ์ ๋ ฅ ์ ์ํ๋ฅผ ์ ๋ฐ์ดํธํ๊ธฐ ์ํด `onChange` ํธ๋ค๋ฌ๋ฅผ ์์ฑํฉ๋๋ค.
- ์ํ ๋ณ์๋ฅผ ์ ๋ ฅ์ `value` prop์ผ๋ก ๋ค์ ์ ๋ฌํฉ๋๋ค.
์ด ๋ฐฉ์์ React๊ฐ ํผ ์ํ๋ฅผ ์์ ํ ์ ์ดํ ์ ์๊ฒ ํด์ฃผ์ง๋ง, ์๋นํ ์์ ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋๋ฅผ ์ ๋ฐํฉ๋๋ค. 10๊ฐ์ ํ๋๊ฐ ์๋ ํผ์ ๊ฒฝ์ฐ, 10๊ฐ์ ์ํ ๋ณ์์ 10๊ฐ์ ํธ๋ค๋ฌ ํจ์๊ฐ ํ์ํ ์ ์์ต๋๋ค. ์ ํจ์ฑ ๊ฒ์ฌ, ์ค๋ฅ ์ํ, ์ ์ถ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๊ฒ์ ํจ์ฌ ๋ ๋ณต์ก์ฑ์ ๊ฐ์ค์ํค๋ฉฐ, ์ข ์ข ๊ฐ๋ฐ์๋ค์ด ๋ณต์กํ ์ปค์คํ ํ ์ ๋ง๋ค๊ฑฐ๋ ํฌ๊ด์ ์ธ ์๋ํํฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ฒ ๋ง๋ญ๋๋ค.
Formik์ด๋ React Hook Form๊ณผ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ์ด๋ฌํ ๋ณต์ก์ฑ์ ์ถ์ํํ์ฌ ํฐ ์ธ๊ธฐ๋ฅผ ์ป์์ต๋๋ค. ์ด๋ค์ ์ํ ๊ด๋ฆฌ, ์ ํจ์ฑ ๊ฒ์ฌ, ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํ ํ๋ฅญํ ์๋ฃจ์ ์ ์ ๊ณตํฉ๋๋ค. ๊ทธ๋ฌ๋ ์ด๋ ๊ด๋ฆฌํด์ผ ํ ๋ ๋ค๋ฅธ ์์กด์ฑ์ด๋ฉฐ, ์ข ์ข ์์ ํ ํด๋ผ์ด์ธํธ ์ธก์์ ์๋ํ์ฌ ํ๋ก ํธ์๋์ ๋ฐฑ์๋ ๊ฐ์ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง์ด ์ค๋ณต๋ ์ ์์ต๋๋ค.
์๋ก์ด ์๋: ์ ์ง์ ํฅ์๊ณผ ์๋ฒ ์ก์
React ์๋ฒ ์ก์ ์ ํจ๋ฌ๋ค์์ ์ ํ์ ๊ฐ์ ธ์ต๋๋ค. ํต์ฌ ์์ด๋์ด๋ ์น ํ๋ซํผ์ ๊ธฐ๋ฐ์ธ ํ์ค HTML `
๊ฐ๋จํ ์์ : ์ค๋งํธ ์ ์ถ ๋ฒํผ
๊ฐ์ฅ ์ผ๋ฐ์ ์ธ ์ฌ์ฉ ์ฌ๋ก๋ฅผ ์ค์ ๋ก ์ดํด๋ณด๊ฒ ์ต๋๋ค. ํ์ค `
ํ์ผ: SubmitButton.js
import { experimental_useFormStatus as useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
);
}
ํ์ผ: SignUpForm.js
import { SubmitButton } from './SubmitButton';
import { signUpAction } from './actions'; // ์๋ฒ ์ก์
export function SignUpForm() {
return (
์ด ์์ ์์ `SubmitButton`์ ์์ ํ ๋ ๋ฆฝ์ ์ ๋๋ค. ์ด๋ ํ prop๋ ๋ฐ์ง ์์ต๋๋ค. `useFormStatus`๋ฅผ ์ฌ์ฉํ์ฌ `SignUpForm`์ด ๋ณด๋ฅ ์ํ์ผ ๋๋ฅผ ํ์ ํ๊ณ ์๋์ผ๋ก ์์ ์ ๋นํ์ฑํํ๊ณ ํ ์คํธ๋ฅผ ๋ณ๊ฒฝํฉ๋๋ค. ์ด๋ ์ปดํฌ๋ํธ๋ฅผ ๋ถ๋ฆฌํ๊ณ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ, ํผ์ ์ธ์งํ๋ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋๋ ๊ฐ๋ ฅํ ํจํด์ ๋๋ค.
๋ฌธ์ ์ ํต์ฌ: ์ํ ๊ธฐ๋ฐ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง
์ด์ ํต์ฌ ๊ฐ๋ ์ ๋๋ฌํ์ต๋๋ค. `useFormStatus`๋ ๋จ์ง ๋ก๋ฉ ์ํ๋ง์ ์ํ ๊ฒ์ด ์๋๋ผ, ์ ํจ์ฑ ๊ฒ์ฌ์ ๋ํ ๋ค๋ฅธ ์ฌ๊ณ ๋ฐฉ์์ ๊ฐ๋ฅํ๊ฒ ํ๋ ํต์ฌ ์์์ ๋๋ค.
"์ํ ๊ฒ์ฆ" ์ ์ํ๊ธฐ
์ํ ๊ธฐ๋ฐ ์ ํจ์ฑ ๊ฒ์ฌ๋ ์ ํจ์ฑ ๊ฒ์ฌ ํผ๋๋ฐฑ์ด ์ฃผ๋ก ํผ ์ ์ถ ์๋์ ๋ํ ์๋ต์ผ๋ก ์ฌ์ฉ์์๊ฒ ์ ๋ฌ๋๋ ํจํด์ ๋๋ค. ๋ชจ๋ ํค ์ ๋ ฅ ์(`onChange`) ๋๋ ์ฌ์ฉ์๊ฐ ํ๋๋ฅผ ๋ ๋ ๋(`onBlur`) ์ ํจ์ฑ์ ๊ฒ์ฌํ๋ ๋์ , ์ฃผ์ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง์ ์ฌ์ฉ์๊ฐ ํผ์ ์ ์ถํ ๋ ์คํ๋ฉ๋๋ค. ์ด ์ ์ถ์ ๊ฒฐ๊ณผ, ์ฆ ๊ทธ *์ํ*(์: ์ฑ๊ณต, ์ ํจ์ฑ ๊ฒ์ฌ ์ค๋ฅ, ์๋ฒ ์ค๋ฅ)๊ฐ UI๋ฅผ ์ ๋ฐ์ดํธํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.
์ด ์ ๊ทผ ๋ฐฉ์์ React ์๋ฒ ์ก์ ๊ณผ ์๋ฒฝํ๊ฒ ์ผ์นํฉ๋๋ค. ์๋ฒ ์ก์ ์ ์ ํจ์ฑ ๊ฒ์ฌ์ ๋ํ ๋จ์ผ ์ง์ค ๊ณต๊ธ์(single source of truth)์ด ๋ฉ๋๋ค. ํผ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ๋น์ฆ๋์ค ๊ท์น(์: "์ด ์ด๋ฉ์ผ์ด ์ด๋ฏธ ์ฌ์ฉ ์ค์ธ๊ฐ?")์ ๋ฐ๋ผ ์ ํจ์ฑ์ ๊ฒ์ฌํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋ํ๋ด๋ ๊ตฌ์กฐํ๋ ์ํ ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค.
๊ทธ ํํธ๋์ ์ญํ : `experimental_useFormState`
`useFormStatus`๋ *๋ฌด์จ ์ผ์ด* ์ผ์ด๋๊ณ ์๋์ง(๋ณด๋ฅ ์ค) ์๋ ค์ฃผ์ง๋ง, ๋ฌด์จ ์ผ์ด ์ผ์ด๋ฌ๋์ง์ ๋ํ *๊ฒฐ๊ณผ*๋ ์๋ ค์ฃผ์ง ์์ต๋๋ค. ์ด๋ฅผ ์ํด ์๋งค ํ ์ธ `experimental_useFormState`๊ฐ ํ์ํฉ๋๋ค.
`useFormState`๋ ํผ ์ก์ ์ ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์ํ๋ฅผ ์ ๋ฐ์ดํธํ๋๋ก ์ค๊ณ๋ ํ ์ ๋๋ค. ์ก์ ํจ์์ ์ด๊ธฐ ์ํ๋ฅผ ์ธ์๋ก ๋ฐ์ ์๋ก์ด ์ํ์ ํผ์ ์ ๋ฌํ ๋ํ๋ ์ก์ ํจ์๋ฅผ ๋ฐํํฉ๋๋ค.
const [state, formAction] = useFormState(myAction, initialState);
- `state`: ์ด๊ฒ์ `myAction`์ ๋ง์ง๋ง ์คํ์ผ๋ก๋ถํฐ์ ๋ฐํ ๊ฐ์ ํฌํจํฉ๋๋ค. ์ฌ๊ธฐ์ ์ค๋ฅ ๋ฉ์์ง๋ฅผ ์ป๊ฒ ๋ ๊ฒ์ ๋๋ค.
- `formAction`: ์ด๊ฒ์ `
`์ `action` prop์ ์ ๋ฌํด์ผ ํ๋ ์๋ก์ด ๋ฒ์ ์ ์ก์ ์ ๋๋ค. ์ด๊ฒ์ด ํธ์ถ๋๋ฉด ์๋ ์ก์ ์ ํธ๋ฆฌ๊ฑฐํ๊ณ `state`๋ฅผ ์ ๋ฐ์ดํธํฉ๋๋ค.
๊ฒฐํฉ๋ ์ํฌํ๋ก: ํด๋ฆญ๋ถํฐ ํผ๋๋ฐฑ๊น์ง
`useFormState`์ `useFormStatus`๊ฐ ํจ๊ป ์๋ํ์ฌ ์์ ํ ์ ํจ์ฑ ๊ฒ์ฌ ๋ฃจํ๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
- ์ด๊ธฐ ๋ ๋๋ง: ํผ์ `useFormState`์์ ์ ๊ณตํ๋ ์ด๊ธฐ ์ํ๋ก ๋ ๋๋ง๋ฉ๋๋ค. ์ค๋ฅ๋ ํ์๋์ง ์์ต๋๋ค.
- ์ฌ์ฉ์ ์ ์ถ: ์ฌ์ฉ์๊ฐ ์ ์ถ ๋ฒํผ์ ํด๋ฆญํฉ๋๋ค.
- ๋ณด๋ฅ ์ํ: ์ ์ถ ๋ฒํผ์ `useFormStatus` ํ ์ด ์ฆ์ `pending: true`๋ฅผ ๋ณด๊ณ ํฉ๋๋ค. ๋ฒํผ์ ๋นํ์ฑํ๋๊ณ ๋ก๋ฉ ๋ฉ์์ง๋ฅผ ํ์ํฉ๋๋ค.
- ์ก์ ์คํ: (`useFormState`์ ์ํด ๋ํ๋) ์๋ฒ ์ก์ ์ด ํผ ๋ฐ์ดํฐ์ ํจ๊ป ์คํ๋ฉ๋๋ค. ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ํํฉ๋๋ค.
- ์ก์
๋ฐํ: ์ก์
์ด ์ ํจ์ฑ ๊ฒ์ฌ์ ์คํจํ๊ณ ์ํ ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค. ์๋ฅผ ๋ค์ด:
`{ message: "์ ํจ์ฑ ๊ฒ์ฌ์ ์คํจํ์ต๋๋ค", errors: { email: "์ด ์ด๋ฉ์ผ์ ์ด๋ฏธ ์ฌ์ฉ ์ค์ ๋๋ค." } }` - ์ํ ์ ๋ฐ์ดํธ: `useFormState`๊ฐ ์ด ๋ฐํ ๊ฐ์ ๋ฐ์ `state` ๋ณ์๋ฅผ ์ ๋ฐ์ดํธํฉ๋๋ค. ์ด๋ ํผ ์ปดํฌ๋ํธ์ ์ฌ๋ ๋๋ง์ ํธ๋ฆฌ๊ฑฐํฉ๋๋ค.
- UI ํผ๋๋ฐฑ: ํผ์ด ์ฌ๋ ๋๋ง๋ฉ๋๋ค. `useFormStatus`์ `pending` ์ํ๋ `false`๊ฐ ๋ฉ๋๋ค. ์ด์ ์ปดํฌ๋ํธ๋ `state.errors.email`์ ์ฝ๊ณ ์ด๋ฉ์ผ ์ ๋ ฅ ํ๋ ์์ ์ค๋ฅ ๋ฉ์์ง๋ฅผ ํ์ํ ์ ์์ต๋๋ค.
์ด ์ ์ฒด ํ๋ฆ์ ์ ์ถ ์ํ์ ๊ฒฐ๊ณผ์ ์ํด ์ ์ ์ผ๋ก ๊ตฌ๋๋๋, ๋ช ํํ๊ณ ์๋ฒ๊ฐ ๊ถ์๋ฅผ ๊ฐ๋ ํผ๋๋ฐฑ์ ์ฌ์ฉ์์๊ฒ ์ ๊ณตํฉ๋๋ค.
์ค์ ๋ง์คํฐํด๋์ค: ๋ค์ค ํ๋ ํ์๊ฐ์ ํผ ๋ง๋ค๊ธฐ
์ด๋ฌํ ๊ฐ๋ ๋ค์ ์ค์ ์ด์ ์์ค์ ์์ ํ ํ์๊ฐ์ ํผ์ ๊ตฌ์ถํ์ฌ ํ๊ณ ํ ํด๋ด ์๋ค. ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ํด ์๋ฒ ์ก์ ์ ์ฌ์ฉํ๊ณ , ํ๋ฅญํ ์ฌ์ฉ์ ๊ฒฝํ์ ๋ง๋ค๊ธฐ ์ํด `useFormState`์ `useFormStatus`๋ฅผ ๋ชจ๋ ์ฌ์ฉํ ๊ฒ์ ๋๋ค.
1๋จ๊ณ: ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ํฌํจํ ์๋ฒ ์ก์ ์ ์ํ๊ธฐ
๋จผ์ , ์๋ฒ ์ก์ ์ด ํ์ํฉ๋๋ค. ๊ฒฌ๊ณ ํ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ํด ์ธ๊ธฐ ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Zod๋ฅผ ์ฌ์ฉํ๊ฒ ์ต๋๋ค. ์ด ์ก์ ์ ๋ณ๋์ ํ์ผ์ ์์นํ๋ฉฐ, Next.js์ ๊ฐ์ ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ `'use server';` ์ง์๋ฌธ์ผ๋ก ํ์๋ฉ๋๋ค.
ํ์ผ: actions/authActions.js
'use server';
import { z } from 'zod';
// ์ ํจ์ฑ ๊ฒ์ฌ ์คํค๋ง ์ ์
const registerSchema = z.object({
username: z.string().min(3, '์ฌ์ฉ์ ์ด๋ฆ์ 3์ ์ด์์ด์ด์ผ ํฉ๋๋ค.'),
email: z.string().email('์ ํจํ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.'),
password: z.string().min(8, '๋น๋ฐ๋ฒํธ๋ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค.'),
});
// ํผ์ ์ด๊ธฐ ์ํ ์ ์
export const initialState = {
message: '',
errors: {},
};
export async function registerUser(prevState, formData) {
// 1. ํผ ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ
const validatedFields = registerSchema.safeParse(
Object.fromEntries(formData.entries())
);
// 2. ์ ํจ์ฑ ๊ฒ์ฌ ์คํจ ์, ์ค๋ฅ ๋ฐํ
if (!validatedFields.success) {
return {
message: '์ ํจ์ฑ ๊ฒ์ฌ์ ์คํจํ์ต๋๋ค. ํ๋๋ฅผ ํ์ธํด์ฃผ์ธ์.',
errors: validatedFields.error.flatten().fieldErrors,
};
}
// 3. (์๋ฎฌ๋ ์ด์
) ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฌ์ฉ์๊ฐ ์ด๋ฏธ ์กด์ฌํ๋์ง ํ์ธ
// ์ค์ ์ฑ์์๋ ์ฌ๊ธฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฟผ๋ฆฌํฉ๋๋ค.
if (validatedFields.data.email === 'user@example.com') {
return {
message: '๊ฐ์
์ ์คํจํ์ต๋๋ค.',
errors: { email: ['์ด๋ฏธ ๋ฑ๋ก๋ ์ด๋ฉ์ผ์
๋๋ค.'] },
};
}
// 4. (์๋ฎฌ๋ ์ด์
) ์ฌ์ฉ์ ์์ฑ
console.log('์ฌ์ฉ์ ์์ฑ ์ค:', validatedFields.data);
// 5. ์ฑ๊ณต ์ํ ๋ฐํ
// ์ค์ ์ฑ์์๋ 'next/navigation'์ `redirect()`๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ๊ธฐ์ ๋ฆฌ๋๋ ์
ํ ์ ์์ต๋๋ค.
return {
message: '์ฌ์ฉ์๊ฐ ์ฑ๊ณต์ ์ผ๋ก ๋ฑ๋ก๋์์ต๋๋ค!',
errors: {},
};
}
์ด ์๋ฒ ์ก์ ์ ์ฐ๋ฆฌ ํผ์ ๋๋์ ๋๋ค. ์ด๊ฒ์ ๋ ๋ฆฝ์ ์ด๊ณ ์์ ํ๋ฉฐ ์ฑ๊ณต ๋ฐ ์ค๋ฅ ์ํ์ ๋ํ ๋ช ํํ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ์ ๊ณตํฉ๋๋ค.
2๋จ๊ณ: ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๊ณ ์ํ๋ฅผ ์ธ์งํ๋ ์ปดํฌ๋ํธ ๊ตฌ์ถํ๊ธฐ
๋ฉ์ธ ํผ ์ปดํฌ๋ํธ๋ฅผ ๊น๋ํ๊ฒ ์ ์งํ๊ธฐ ์ํด, ์ ๋ ฅ๊ณผ ์ ์ถ ๋ฒํผ์ ์ํ ์ ์ฉ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค ๊ฒ์ ๋๋ค.
ํ์ผ: components/SubmitButton.js
'use client';
import { experimental_useFormStatus as useFormStatus } from 'react-dom';
export function SubmitButton({ label }) {
const { pending } = useFormStatus();
return (
);
}
`aria-disabled={pending}` ์ฌ์ฉ์ ์ฃผ๋ชฉํ์ธ์. ์ด๊ฒ์ ์คํฌ๋ฆฐ ๋ฆฌ๋๊ฐ ๋นํ์ฑํ ์ํ๋ฅผ ์ ํํ๊ฒ ์๋ฆฌ๋๋ก ๋ณด์ฅํ๋ ์ค์ํ ์ ๊ทผ์ฑ ์ค์ฒ ๋ฐฉ๋ฒ์ ๋๋ค.
3๋จ๊ณ: `useFormState`๋ก ๋ฉ์ธ ํผ ์กฐ๋ฆฝํ๊ธฐ
์ด์ ๋ฉ์ธ ํผ ์ปดํฌ๋ํธ์์ ๋ชจ๋ ๊ฒ์ ํตํฉํด ๋ณด๊ฒ ์ต๋๋ค. `useFormState`๋ฅผ ์ฌ์ฉํ์ฌ UI๋ฅผ `registerUser` ์ก์ ์ ์ฐ๊ฒฐํ ๊ฒ์ ๋๋ค.
ํ์ผ: components/RegistrationForm.js
{state.message} {state.message}
{state.errors.username[0]}
{state.errors.email[0]}
{state.errors.password[0]}
'use client';
import { experimental_useFormState as useFormState } from 'react-dom';
import { registerUser, initialState } from '../actions/authActions';
import { SubmitButton } from './SubmitButton';
export function RegistrationForm() {
const [state, formAction] = useFormState(registerUser, initialState);
return (
ํ์๊ฐ์
{state?.message && !state.errors &&
์ด ์ปดํฌ๋ํธ๋ ์ด์ ์ ์ธ์ ์ด๊ณ ๊น๋ํฉ๋๋ค. `useFormState`๊ฐ ์ ๊ณตํ๋ `state` ๊ฐ์ฒด ์ธ์๋ ์ด๋ค ์ํ๋ ์ง์ ๊ด๋ฆฌํ์ง ์์ต๋๋ค. ๊ทธ ์ ์ผํ ์๋ฌด๋ ํด๋น ์ํ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก UI๋ฅผ ๋ ๋๋งํ๋ ๊ฒ์ ๋๋ค. ๋ฒํผ ๋นํ์ฑํ ๋ก์ง์ `SubmitButton`์ ์บก์ํ๋์ด ์์ผ๋ฉฐ, ๋ชจ๋ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง์ `authActions.js`์ ์์ต๋๋ค. ์ด๋ฌํ ๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ๋ ์ ์ง๋ณด์์ฑ ์ธก๋ฉด์์ ํฐ ์ด์ ์ ๋๋ค.
๊ณ ๊ธ ๊ธฐ์ ๋ฐ ์ ๋ฌธ์ ์ธ ๋ชจ๋ฒ ์ฌ๋ก
๊ธฐ๋ณธ ํจํด์ด ๊ฐ๋ ฅํ์ง๋ง, ์ค์ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ์ข ์ข ๋ ๋ง์ ๋ฏธ๋ฌํจ์ด ํ์ํฉ๋๋ค. ๋ช ๊ฐ์ง ๊ณ ๊ธ ๊ธฐ์ ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
ํ์ด๋ธ๋ฆฌ๋ ์ ๊ทผ๋ฒ: ์ฆ๊ฐ์ ์ธ ๊ฒ์ฆ๊ณผ ์ ์ถ ํ ๊ฒ์ฆ์ ๊ฒฐํฉ
์ํ ๊ธฐ๋ฐ ์ ํจ์ฑ ๊ฒ์ฌ๋ ์๋ฒ ์ธก ํ์ธ์ ํ๋ฅญํ์ง๋ง, ์ฌ์ฉ์์ ์ด๋ฉ์ผ์ด ์ ํจํ์ง ์๋ค๊ณ ์๋ฆฌ๊ธฐ ์ํด ๋คํธ์ํฌ ์๋ณต์ ๊ธฐ๋ค๋ฆฌ๋ ๊ฒ์ ๋๋ฆด ์ ์์ต๋๋ค. ํ์ด๋ธ๋ฆฌ๋ ์ ๊ทผ๋ฒ์ด ์ข ์ข ์ต์ ์ ๋๋ค:
- HTML5 ์ ํจ์ฑ ๊ฒ์ฌ ์ฌ์ฉ: ๊ธฐ๋ณธ์ ์์ง ๋ง์ธ์! `required`, `type="email"`, `minLength`, `pattern`๊ณผ ๊ฐ์ ์์ฑ์ ๋น์ฉ ์์ด ์ฆ๊ฐ์ ์ธ ๋ธ๋ผ์ฐ์ ๋ค์ดํฐ๋ธ ํผ๋๋ฐฑ์ ์ ๊ณตํฉ๋๋ค.
- ๊ฐ๋ฒผ์ด ํด๋ผ์ด์ธํธ ์ธก ์ ํจ์ฑ ๊ฒ์ฌ: ์์ ํ ์ธ๊ด์ ๋๋ ํ์ ํ์ธ(์: ๋น๋ฐ๋ฒํธ ๊ฐ๋ ํ์๊ธฐ)์ ์ํด ์ต์ํ์ `useState`์ `onChange` ํธ๋ค๋ฌ๋ฅผ ์ฌ์ ํ ์ฌ์ฉํ ์ ์์ต๋๋ค.
- ์๋ฒ ์ธก ๊ถํ: ํด๋ผ์ด์ธํธ์์ ์ํํ ์ ์๋ ๊ฐ์ฅ ์ค์ํ๊ณ ๋น์ฆ๋์ค ๋ก์ง์ ๊ธฐ๋ฐํ ์ ํจ์ฑ ๊ฒ์ฌ(์: ๊ณ ์ ํ ์ฌ์ฉ์ ์ด๋ฆ ํ์ธ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ธฐ๋ก์ ๋ํ ์ ํจ์ฑ ๊ฒ์ฌ)๋ ์๋ฒ ์ก์ ์ ๋งก๊น๋๋ค.
์ด๋ ๋จ์ํ ์ค๋ฅ์ ๋ํ ์ฆ๊ฐ์ ์ธ ํผ๋๋ฐฑ๊ณผ ๋ณต์กํ ๊ท์น์ ๋ํ ๊ถ์ ์๋ ์ ํจ์ฑ ๊ฒ์ฌ๋ผ๋ ๋ ์ธ๊ณ์ ์ฅ์ ์ ๋ชจ๋ ์ ๊ณตํฉ๋๋ค.
์ ๊ทผ์ฑ (A11y): ๋ชจ๋๋ฅผ ์ํ ํผ ๋ง๋ค๊ธฐ
์ ๊ทผ์ฑ์ ํํํ ์ ์๋ ๋ถ๋ถ์ ๋๋ค. ์ํ ๊ธฐ๋ฐ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ๊ตฌํํ ๋ ๋ค์ ์ฌํญ์ ๋ช ์ฌํ์ธ์:
- ์ค๋ฅ ์๋ฆผ: ์์ ์์๋ ์ค๋ฅ ๋ฉ์์ง ์ปจํ ์ด๋์ `aria-live="polite"`๋ฅผ ์ฌ์ฉํ์ต๋๋ค. ์ด๋ ์คํฌ๋ฆฐ ๋ฆฌ๋์๊ฒ ์ฌ์ฉ์์ ํ์ฌ ํ๋ฆ์ ๋ฐฉํดํ์ง ์์ผ๋ฉด์ ์ค๋ฅ ๋ฉ์์ง๊ฐ ๋ํ๋๋ ์ฆ์ ์๋ฆฌ๋๋ก ์ง์ํฉ๋๋ค.
- ์ค๋ฅ์ ์ ๋ ฅ ์ฐ๊ด์ํค๊ธฐ: ๋ ๊ฒฌ๊ณ ํ ์ฐ๊ฒฐ์ ์ํด `aria-describedby` ์์ฑ์ ์ฌ์ฉํ์ธ์. ์ ๋ ฅ์ ์ค๋ฅ ๋ฉ์์ง ์ปจํ ์ด๋์ ID๋ฅผ ๊ฐ๋ฆฌ์ผ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ ๋งํฌ๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
- ํฌ์ปค์ค ๊ด๋ฆฌ: ์ค๋ฅ๊ฐ ์๋ ์ ์ถ ํ, ์ฒซ ๋ฒ์งธ ์ ํจํ์ง ์์ ํ๋๋ก ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ํฌ์ปค์ค๋ฅผ ์ด๋ํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์. ์ด๋ ์ฌ์ฉ์๊ฐ ๋ฌด์์ด ์๋ชป๋์๋์ง ์ฐพ๋ ์๊ณ ๋ฅผ ๋์ด์ค๋๋ค.
`useFormStatus`์ `data` ์์ฑ์ ์ด์ฉํ ๋๊ด์ UI
์ฌ์ฉ์๊ฐ ๋๊ธ์ ๊ฒ์ํ๋ ์์ ๋ฏธ๋์ด ์ฑ์ ์์ํด๋ณด์ธ์. 1์ด ๋์ ์คํผ๋๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋์ , ์ฑ์ด ์ฆ๊ฐ์ ์ผ๋ก ๋๊ปด์ง๊ฒ ๋ง๋ค ์ ์์ต๋๋ค. `useFormStatus`์ `data` ์์ฑ์ ์ด๋ฅผ ์ํด ์๋ฒฝํฉ๋๋ค.
ํผ์ด ์ ์ถ๋๋ฉด `pending`์ true๊ฐ ๋๊ณ `data`๋ ์ ์ถ๋ `FormData`๋ก ์ฑ์์ง๋๋ค. ์ด `data`๋ฅผ ์ฌ์ฉํ์ฌ ์ฆ์ ์ ๋๊ธ์ ์์์ ์ธ '๋ณด๋ฅ' ์๊ฐ์ ์ํ๋ก ๋ ๋๋งํ ์ ์์ต๋๋ค. ์๋ฒ ์ก์ ์ด ์ฑ๊ณตํ๋ฉด ๋ณด๋ฅ ์ค์ธ ๋๊ธ์ ์๋ฒ์์ ์จ ์ต์ข ๋ฐ์ดํฐ๋ก ๊ต์ฒดํฉ๋๋ค. ์คํจํ๋ฉด ๋ณด๋ฅ ์ค์ธ ๋๊ธ์ ์ ๊ฑฐํ๊ณ ์ค๋ฅ๋ฅผ ํ์ํ ์ ์์ต๋๋ค. ์ด๋ ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋๋๋๋ก ๋ฐ์์ ์ผ๋ก ๋๊ปด์ง๊ฒ ๋ง๋ญ๋๋ค.
"์คํ์ (Experimental)" ๊ธฐ๋ฅ ๋ค๋ฃจ๊ธฐ
`experimental_useFormStatus`์ `experimental_useFormState`์ "์คํ์ " ์ ๋์ฌ๋ฅผ ๋ค๋ฃจ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
"์คํ์ "์ด ์ค์ ๋ก ์๋ฏธํ๋ ๊ฒ
React๊ฐ API๋ฅผ ์คํ์ ์ผ๋ก ํ์ํ ๋, ์ด๋ ๋ค์์ ์๋ฏธํฉ๋๋ค:
- API๊ฐ ๋ณ๊ฒฝ๋ ์ ์์: ์ด๋ฆ, ์ธ์ ๋๋ ๋ฐํ ๊ฐ์ด ํ์ค ์๋งจํฑ ๋ฒ์ ๋(SemVer)์ ๋ฐ๋ฅด์ง ์๊ณ ํฅํ React ๋ฆด๋ฆฌ์ค์์ ๋ณ๊ฒฝ๋ ์ ์์ต๋๋ค.
- ๋ฒ๊ทธ๊ฐ ์์ ์ ์์: ์๋ก์ด ๊ธฐ๋ฅ์ผ๋ก์, ์์ง ์์ ํ ์ดํด๋๊ฑฐ๋ ํด๊ฒฐ๋์ง ์์ ์ฃ์ง ์ผ์ด์ค๊ฐ ์์ ์ ์์ต๋๋ค.
- ๋ฌธ์๊ฐ ๋ถ์กฑํ ์ ์์: ํต์ฌ ๊ฐ๋ ์ ๋ฌธ์ํ๋์ด ์์ง๋ง, ๊ณ ๊ธ ํจํด์ ๋ํ ์์ธํ ๊ฐ์ด๋๋ ์์ง ๋ฐ์ ์ค์ผ ์ ์์ต๋๋ค.
์ธ์ ๋์ ํ๊ณ ์ธ์ ๊ธฐ๋ค๋ ค์ผ ํ๋๊ฐ
๊ทธ๋ ๋ค๋ฉด, ํ๋ก์ ํธ์์ ์ฌ์ฉํด์ผ ํ ๊น์? ๋๋ต์ ์ํฉ์ ๋ฐ๋ผ ๋ค๋ฆ ๋๋ค:
- ์ ํฉํ ๊ฒฝ์ฐ: ๊ฐ์ธ ํ๋ก์ ํธ, ๋ด๋ถ ๋๊ตฌ, ์คํํธ์ ๋๋ ์ ์ฌ์ ์ธ API ๋ณ๊ฒฝ ๊ด๋ฆฌ์ ์ต์ํ ํ. Next.js์ ๊ฐ์ ํ๋ ์์ํฌ(์ด ๊ธฐ๋ฅ๋ค์ App Router์ ํตํฉํจ) ๋ด์์ ์ฌ์ฉํ๋ ๊ฒ์ ์ผ๋ฐ์ ์ผ๋ก ๋ ์์ ํ ์ ํ์ ๋๋ค. ํ๋ ์์ํฌ๊ฐ ์ผ๋ถ ๋ณ๊ฒฝ ์ฌํญ์ ์ถ์ํํ๋ ๋ฐ ๋์์ด ๋ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
- ์ฃผ์ํด์ ์ฌ์ฉํด์ผ ํ ๊ฒฝ์ฐ: ๋๊ท๋ชจ ์ํฐํ๋ผ์ด์ฆ ์ ํ๋ฆฌ์ผ์ด์ , ๋ฏธ์ ํฌ๋ฆฌํฐ์ปฌ ์์คํ ๋๋ API ์์ ์ฑ์ด ๊ฐ์ฅ ์ค์ํ ์ฅ๊ธฐ ์ ์ง๋ณด์ ๊ณ์ฝ์ด ์๋ ํ๋ก์ ํธ. ์ด๋ฌํ ๊ฒฝ์ฐ, ํ ์ด ์์ ์ ์ธ API๋ก ์น๊ฒฉ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ๋ ๊ฒ์ด ํ๋ช ํ ์ ์์ต๋๋ค.
์ด๋ฌํ ํ ์ ์์ ํ์ ๊ดํ ๋ฐํ๋ฅผ ์ํด ํญ์ ๊ณต์ React ๋ธ๋ก๊ทธ์ ๋ฌธ์๋ฅผ ์ฃผ์ํ์ธ์.
๊ฒฐ๋ก : React ํผ์ ๋ฏธ๋
`experimental_useFormStatus`์ ๊ด๋ จ API์ ๋์ ์ ์๋ก์ด ๋๊ตฌ ๊ทธ ์ด์์ ๋๋ค. ์ด๋ ์ฐ๋ฆฌ๊ฐ React๋ก ์ํธ์์ฉ์ ์ธ ๊ฒฝํ์ ๊ตฌ์ถํ๋ ๋ฐฉ์์ ๋ํ ์ฒ ํ์ ์ ํ์ ๋ํ๋ ๋๋ค. ์น ํ๋ซํผ์ ๊ธฐ๋ฐ์ ์์ฉํ๊ณ ์ํ ์ ์ฅ ๋ก์ง์ ์๋ฒ์ ํจ๊ป ๋ฐฐ์นํจ์ผ๋ก์จ, ์ฐ๋ฆฌ๋ ๋ ๋จ์ํ๊ณ , ๋ ๋ณต์๋ ฅ ์์ผ๋ฉฐ, ์ข ์ข ๋ ์ฑ๋ฅ์ด ์ข์ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ ์ ์์ต๋๋ค.
์ฐ๋ฆฌ๋ `useFormStatus`๊ฐ ์ปดํฌ๋ํธ๊ฐ ํผ ์ ์ถ์ ์๋ช ์ฃผ๊ธฐ์ ๋ฐ์ํ๋ ๊น๋ํ๊ณ ๋ถ๋ฆฌ๋ ๋ฐฉ๋ฒ์ ์ด๋ป๊ฒ ์ ๊ณตํ๋์ง ๋ณด์์ต๋๋ค. ์ด๋ ๋ณด๋ฅ ์ํ์ ๋ํ prop drilling์ ์ ๊ฑฐํ๊ณ ์ค๋งํธํ `SubmitButton`๊ณผ ๊ฐ์ ์ฐ์ํ๊ณ ๋ ๋ฆฝ์ ์ธ UI ์ปดํฌ๋ํธ๋ฅผ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค. `useFormState`์ ๊ฒฐํฉ๋ ๋, ์ด๋ ์๋ฒ๊ฐ ๊ถ๊ทน์ ์ธ ๊ถ์์์ด๊ณ ํด๋ผ์ด์ธํธ์ ์ฃผ์ ์ฑ ์์ ์๋ฒ ์ก์ ์ด ๋ฐํํ ์ํ๋ฅผ ๋ ๋๋งํ๋ ๊ฒ์ธ ์ํ ๊ธฐ๋ฐ ์ ํจ์ฑ ๊ฒ์ฌ์ ๊ฐ๋ ฅํ ํจํด์ ์ด์ด์ค๋๋ค.
"์คํ์ " ํ๊ทธ๋ ์ด๋ ์ ๋์ ์ฃผ์๋ฅผ ์ํ์ง๋ง, ๋ฐฉํฅ์ ๋ถ๋ช ํฉ๋๋ค. React ํผ์ ๋ฏธ๋๋ ์ ์ง์ ํฅ์, ๋จ์ํ๋ ์ํ ๊ด๋ฆฌ, ๊ทธ๋ฆฌ๊ณ ํด๋ผ์ด์ธํธ์ ์๋ฒ ๋ก์ง ๊ฐ์ ๊ฐ๋ ฅํ๊ณ ์ํํ ํตํฉ์ ์๋์ ๋๋ค. ์ค๋ ์ด ์๋ก์ด ํ ๋ค์ ๋ง์คํฐํจ์ผ๋ก์จ, ์ฌ๋ฌ๋ถ์ ๋จ์ํ ์๋ก์ด API๋ฅผ ๋ฐฐ์ฐ๋ ๊ฒ์ด ์๋๋ผ, React๋ฅผ ์ฌ์ฉํ ์ฐจ์ธ๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ์ ์ค๋นํ๋ ๊ฒ์ ๋๋ค.